package org.apache.solr.ocean.core;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Searcher;
import org.apache.solr.ocean.cache.CacheKey;
import org.apache.solr.ocean.core.DiskIndex.DiskIndexSnapshot;
import org.apache.solr.ocean.core.Index.IndexSnapshot;
import org.apache.solr.ocean.core.RamIndex.RamIndexSnapshot;
import org.apache.solr.ocean.core.Snapshot.SnapshotInfo.IndexInfo;
import org.apache.solr.ocean.core.TransactionSystem.Category;
import org.apache.solr.ocean.core.WriteableMemoryIndex.MemoryIndexSnapshot;
import org.apache.solr.ocean.util.CElement;
import org.apache.solr.ocean.util.XMLUtil;
import org.apache.solr.schema.IndexSchema;
import org.jdom.Element;

public class Snapshot {
  private BigDecimal id;
  private SortedMap<Long,IndexSnapshot> indexSnapshotMap;
  private MemoryIndexSnapshot writeableSnapshot;
  private IndexSchema indexSchema;
  private IndexReader indexReader;
  private Map<IndexReader,IndexSnapshot> readerMap;
  private int maxDoc;
  private int[] starts;
  private TransactionSystem.Category category;

  public Snapshot(Long snapshotID, int minorVersion, TransactionSystem.Category category, MemoryIndexSnapshot writeableSnapshot,
      List<IndexSnapshot> indexSnapshots, IndexSchema indexSchema) throws IOException {
    id = toId(snapshotID, minorVersion);
    this.category = category;
    this.writeableSnapshot = writeableSnapshot;
    this.indexSchema = indexSchema;
    assert snapshotIdsMatch(indexSnapshots);
    indexSnapshotMap = new TreeMap<Long,IndexSnapshot>();
    readerMap = new HashMap<IndexReader,IndexSnapshot>(indexSnapshots.size());
    IndexReader[] readerArray = new IndexReader[indexSnapshots.size()];
    int x=0;
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      indexSnapshotMap.put(indexSnapshot.getIndex().getId(), indexSnapshot);
      readerMap.put(indexSnapshot.getIndexReader(), indexSnapshot);
      readerArray[x] = indexSnapshot.getIndexReader();
      x++;
    }
    IndexSnapshot[] indexSnapshotsArray = indexSnapshotMap.values().toArray(new IndexSnapshot[0]);
    int[] starts = new int[indexSnapshotsArray.length + 1]; // build starts
                                                            // array
    for (int i = 0; i < indexSnapshotsArray.length; i++) {
      starts[i] = maxDoc;
      maxDoc += indexSnapshotsArray[i].maxDoc(); // compute maxDocs
    }
    starts[indexSnapshotsArray.length] = maxDoc;
  }
  
  public Searcher[] getSearchers() {
    IndexSnapshot[] indexSnapshots = (IndexSnapshot[])indexSnapshotMap.values().toArray(new IndexSnapshot[0]);
    Searcher[] searchers = new Searcher[indexSnapshotMap.size()];
    for (int x=0; x < indexSnapshots.length; x++) {
      searchers[x] = new IndexSearcher(indexSnapshots[x].getIndexReader());
    }
    return searchers;
  }
  
  public IndexReader getIndexReader() {
    return indexReader;
  }

  public int maxDoc() {
    return maxDoc;
  }

  public int[] getStarts() {
    return starts;
  }

  public IndexSnapshot getIndexSnapshot(IndexReader indexReader) {
    return readerMap.get(indexReader);
  }

  public IndexSchema getIndexSchema() {
    return indexSchema;
  }

  public List<RamIndexSnapshot> getRamIndexSnapshots() {
    List<RamIndexSnapshot> ramIndexSnapshots = new ArrayList<RamIndexSnapshot>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      if (indexSnapshot instanceof RamIndexSnapshot) {
        ramIndexSnapshots.add((RamIndexSnapshot) indexSnapshot);
      }
    }
    return ramIndexSnapshots;
  }

  public IndexReader[] getIndexReaders() {
    IndexReader[] indexReaders = new IndexReader[indexSnapshotMap.size()];
    int i = 0;
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      indexReaders[i] = indexSnapshot.getIndexReader();
      i++;
    }
    return indexReaders;
  }

  public int getMaxDoc() {
    int maxDoc = 0;
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      maxDoc += indexSnapshot.getIndexReader().maxDoc();
    }
    return maxDoc;
  }

  public Category getCategory() {
    return category;
  }

  public int getMinorVersion() {
    return getMinorVersion(id);
  }

  public static BigDecimal toId(Long snapshotId, int minorVersion) {
    StringBuilder builder = new StringBuilder();
    builder.append(snapshotId);
    builder.append(".");
    if (10 > minorVersion)
      builder.append("0");
    builder.append(minorVersion);
    BigDecimal value = new BigDecimal(builder.toString());
    return value;
  }

  public static int getMinorVersion(BigDecimal value) {
    value = value.subtract(new BigDecimal(value.longValue()));
    BigDecimal decimal = value.scaleByPowerOfTen(2);
    return decimal.intValue();
  }

  public static boolean snapshotIdsMatch(Collection<IndexSnapshot> indexSnapshots) {
    Long current = null;
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      if (current == null) {
        current = indexSnapshot.getSnapshotId();
      } else if (!current.equals(indexSnapshot.getSnapshotId())) {
        return false;
      }
    }
    return true;
  }

  public Snapshot(BigDecimal id, MemoryIndexSnapshot writeableSnapshot, Collection<IndexSnapshot> indexSnapshots, Category category)
      throws IOException {
    this.id = id;
    this.writeableSnapshot = writeableSnapshot;
    this.category = category;
    assert snapshotIdsMatch(indexSnapshots);
    indexSnapshotMap = new TreeMap<Long,IndexSnapshot>();
    for (IndexSnapshot indexSnapshot : indexSnapshots) {
      indexSnapshotMap.put(indexSnapshot.getIndex().getId(), indexSnapshot);
    }
  }

  public SnapshotInfo getSnapshotInfo() {
    SnapshotInfo snapshotInfo = new SnapshotInfo(id, indexSchema.getVersion());
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      Index index = indexSnapshot.getIndex();
      String type = null;
      if (index instanceof DiskIndex)
        type = "disk";
      if (index instanceof WriteableMemoryIndex)
        type = "memory";
      if (index instanceof RamIndex)
        type = "ram";
      IndexInfo indexInfo = new IndexInfo(index.getId(), type);
      snapshotInfo.add(indexInfo);
    }
    return snapshotInfo;
  }

  public static SnapshotInfo loadHighestSnapshotInfo(File directoryFile) throws Exception {
    TreeSet<BigDecimal> sortedSet = new TreeSet<BigDecimal>();
    for (File file : directoryFile.listFiles()) {
      if (!file.isDirectory()) {
        String name = file.getName();
        String str = "snapshot_";
        if (name.startsWith(str)) {
          String main = name.substring(str.length() - 1, name.lastIndexOf('.'));
          String[] split = StringUtils.split(main);
          Long snapshotId = new Long(split[0]);
          Integer version = new Integer(split[1]);
          sortedSet.add(new BigDecimal(snapshotId + "." + version));
        }
      }
    }
    BigDecimal highestId = sortedSet.last();
    if (highestId == null)
      return null;
    String fileName = getFileName(highestId);
    File file = new File(directoryFile, fileName);
    Element element = XMLUtil.parseElement(file);
    return new SnapshotInfo(element);
  }

  public static String getFileName(BigDecimal id) {
    String string = id.toString();
    String replaced = string.replace('.', '_');
    return "snapshot_" + replaced + ".xml";
  }

  public static class SnapshotInfo implements CElement {
    private BigDecimal id;
    private Float schemaVersion;
    private Map<Long,IndexInfo> indexInfos;

    public SnapshotInfo(BigDecimal id, Float schemaVersion) {
      this.id = id;
      this.schemaVersion = schemaVersion;
      indexInfos = new HashMap<Long,IndexInfo>();
    }

    public void add(IndexInfo indexInfo) {
      indexInfos.put(indexInfo.getId(), indexInfo);
    }

    public SnapshotInfo(Element element) {
      indexInfos = new HashMap<Long,IndexInfo>();
      id = new BigDecimal(element.getAttributeValue("id"));
      schemaVersion = XMLUtil.getAttributeFloat("schemaversion", element);
      for (Element indexElement : XMLUtil.getChildren("index", element)) {
        IndexInfo indexInfo = new IndexInfo(indexElement);
        indexInfos.put(indexInfo.getId(), indexInfo);
      }
    }

    public Float getSchemaVersion() {
      return schemaVersion;
    }

    public Long getSnapshotId() {
      return id.longValue();
    }

    public BigDecimal getId() {
      return id;
    }

    public Collection<IndexInfo> getIndexInfos() {
      return indexInfos.values();
    }

    public SnapshotInfo(BigDecimal id, Map<Long,IndexInfo> indexInfos) {
      this.id = id;
      this.indexInfos = indexInfos;
    }

    public static class IndexInfo implements CElement {
      private Long id;
      private String type;

      public IndexInfo(Long id, String type) {
        this.id = id;
        this.type = type;
      }

      public IndexInfo(Element element) {
        id = XMLUtil.getAttributeLong("id", element);
        type = XMLUtil.getAttributeString("type", element);
      }

      public Long getId() {
        return id;
      }

      public String getType() {
        return type;
      }

      public Element toElement() {
        Element element = new Element("index");
        XMLUtil.setAttribute("id", id, element);
        XMLUtil.setAttribute("type", type, element);
        return element;
      }
    }

    public void writeTo(File file) throws IOException {
      Element element = toElement();
      XMLUtil.saveXML(element, file);
    }

    public Element toElement() {
      Element element = new Element("snapshot");
      XMLUtil.setAttribute("id", id, element);
      for (IndexInfo indexInfo : indexInfos.values()) {
        element.addContent(indexInfo.toElement());
      }
      return element;
    }
  }

  public Snapshot createMinor(List<Long> removeIndexIds, IndexSnapshot newIndexSnapshot) throws IOException {
    return createMinor(removeIndexIds, writeableSnapshot, newIndexSnapshot);
  }

  public Snapshot createMinor(List<Long> removeIndexIds, MemoryIndexSnapshot writeableSnapshot, IndexSnapshot newIndexSnapshot)
      throws IOException {
    HashMap<Long,IndexSnapshot> mapCopy = new HashMap<Long,IndexSnapshot>(indexSnapshotMap);
    for (Long indexid : removeIndexIds) {
      mapCopy.remove(indexid);
    }
    Long newIndexId = newIndexSnapshot.getIndex().getId();
    assert !mapCopy.containsKey(newIndexId);
    mapCopy.put(newIndexId, newIndexSnapshot);
    int minorVersion = getMinorVersion();
    Long snapshotID = getSnapshotId();
    BigDecimal newId = toId(snapshotID, minorVersion);
    Snapshot newSnapshot = new Snapshot(newId, writeableSnapshot, mapCopy.values(), category);
    return newSnapshot;
  }

  public List<DiskIndex> getDiskIndices() {
    List<DiskIndex> diskIndices = new ArrayList<DiskIndex>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      indexSnapshot.getIndex();
    }
    return diskIndices;
  }

  public List<Index> getDeleteOnlyIndices() {
    HashMap<Long,IndexSnapshot> mapCopy = new HashMap<Long,IndexSnapshot>(indexSnapshotMap);
    mapCopy.remove(writeableSnapshot.getIndex().getId());
    List<Index> indices = new ArrayList<Index>();
    for (IndexSnapshot indexSnapshot : mapCopy.values()) {
      indices.add(indexSnapshot.getIndex());
    }
    return indices;
  }

  public MemoryIndexSnapshot getWriteableSnapshot() {
    return writeableSnapshot;
  }

  public Set<CacheKey> getCacheKeys() {
    Set<CacheKey> cacheKeys = new HashSet<CacheKey>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      cacheKeys.addAll(indexSnapshot.getCacheKeys());
    }
    return cacheKeys;
  }

  public boolean containsIndex(long indexid) {
    return indexSnapshotMap.containsKey(indexid);
  }

  public List<DiskIndexSnapshot> getDiskIndexSnapshots() {
    List<DiskIndexSnapshot> diskIndexSnapshots = new ArrayList<DiskIndexSnapshot>();
    for (IndexSnapshot indexSnapshot : indexSnapshotMap.values()) {
      if (indexSnapshot instanceof DiskIndexSnapshot) {
        diskIndexSnapshots.add((DiskIndexSnapshot) indexSnapshot);
      }
    }
    return diskIndexSnapshots;
  }

  public List<IndexSnapshot> getIndexSnapshots() {
    return new ArrayList(indexSnapshotMap.values());
  }

  public Long getSnapshotId() {
    return id.longValue();
  }

  public BigDecimal getId() {
    return id;
  }
}
